%% -*- coding: utf-8 -*-

%% @doc a scanner for the Templerl expression language
%%
-module(templel_scan).
-export([string/1,string/2,tokens/3,scan/7,format_error/1]).

-import(lists, [reverse/1]).

-include_lib("../include/templ_scan.hrl").

-define(IsHex(H),      (((H >= $0) and (H =< $9)) or ((H >= $A) and (H =< $F)))).
-define(IsHexGT(H, L), (((H >= L)  and (H =< $9)) or ((H >= $A) and (H =< $F)))).


%% @spec string([char()], integer()) -> scan_result()
%%
string(Cs, Pos) when is_list(Cs), is_integer(Pos) ->
    %% Debug replacement line for chopping string into 1-char segments
%    scan([], [], [], ?InitPos(Pos), Cs, [], ?RDelim).
    scan(Cs, [], [], ?InitPos(Pos), [], [], ?RDelim).


%%% scan utils

%% @doc finish the scanning process
%%
%% String
done(eof, [], Toks, Pos, eos) ->
    {ok,reverse(Toks),?GetPos(Pos)};
done(eof, Errors, _Toks, Pos, eos) ->
    {Error,ErrorPos} = lists:last(Errors),
    {error,{ErrorPos,?MODULE,Error},?GetPos(Pos)};
done(Cs, Errors, Toks, Pos, eos) ->
    main_scan(Cs, [], Toks, Pos, eos, Errors, ?RDelim);
% %% Debug clause for chopping string into 1-char segments
% done(Cs, Errors, Toks, Pos, [H|T], Fun) ->
%    Fun(Cs++[H], [], Toks, Pos, T, Errors);
done(Cs, Errors, Toks, Pos, []) ->
    main_scan(Cs++eof, [], Toks, Pos, eos, Errors, ?RDelim);
%% Stream
done(Cs, [], [{dot,_}|_]=Toks, Pos, io) ->
    {done,{ok,reverse(Toks),?GetPos(Pos)},Cs};
done(Cs, [], [_|_], Pos, io) ->
    {done,{error,{Pos,?MODULE,scan},?GetPos(Pos)},Cs};
done(Cs, [], [], Pos, eof) ->
    {done,{eof,?GetPos(Pos)},Cs};
done(Cs, [], [{dot,_}|_]=Toks, Pos, eof) ->
    {done,{ok,reverse(Toks),?GetPos(Pos)},Cs};
done(Cs, [], _Toks, Pos, eof) ->
    {done,{error,{Pos,?MODULE,scan},?GetPos(Pos)},Cs};
done(Cs, Errors, _Toks, Pos, io) ->
    {Error,ErrorPos} = lists:last(Errors),
    {done,{error,{ErrorPos,?MODULE,Error},?GetPos(Pos)},Cs};
done(Cs, Errors, _Toks, Pos, eof) ->
    {Error,ErrorPos} = lists:last(Errors),
    {done,{error,{ErrorPos,?MODULE,Error},?GetPos(Pos)},Cs}.


%% @spec scan(stream(), stack(), [tok()], pos(), state(), errors(), string()) 
%%         -> result()
%% @doc top-level of scan
%%
scan([$*|Cs], Stack, Toks=[{l_delim,_}|_], Pos, State, Errors, Delim) ->
	scan_comment(Cs, Stack, Toks, Pos, State, Errors, Delim);
scan(Cs, Stack, Toks, Pos, State, Errors, Delim) ->
	scan_rec(Cs, Stack, Toks, Pos, State, Errors, Delim).


%% @doc scan for a comment
%%
scan_comment([$*|Cs], _Stack, Toks, Pos, State, Errors, Delim) ->
	%sub_scan_delim(Cs, Stack, Toks, Pos, State, Errors, Delim, fun scan_comment/8);
	sub_scan_delim(Cs, [fun scan_comment/7], Toks, Pos, State, Errors, Delim);
scan_comment([_|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
	scan_comment(Cs, Stack, Toks, Pos, State, Errors, Delim);
scan_comment([], Stack, Toks, Pos, State, Errors, Delim) ->
    more([], Stack, Toks, Pos, State, Errors, Delim, fun scan_comment/7);
scan_comment(Eof, _Stack, Toks, Pos, State, Errors, _Delim) ->
    done(Eof, [{illegal,comment}|Errors], Toks, Pos, State).


%% @doc an interface for tail-recursive call
%%
scan_rec(Cs, _Stack, Toks, Pos, State, Errors, Delim) ->
	sub_scan_delim(Cs, [fun main_scan/7], Toks, Pos, State, Errors, Delim).


%% @doc scan for a delimiter
%%
sub_scan_delim([C|Cs], Stack, Toks, Pos, State, Errors, [DC|DCs]) 
  when C =:= DC ->
	case Stack of
		[Fun] -> sub_scan_delim(Cs, [C,Pos,Fun], Toks, Pos, State, Errors, DCs);
		_     -> sub_scan_delim(Cs, [C|Stack], Toks, Pos, State, Errors, DCs)
	end;
sub_scan_delim(Cs, _Stack, Toks, Pos, State, Errors, []) ->
	Parent = ?PopPos(Pos),
	case Parent of
		[] -> done(eof, Errors, [{rdelim,Pos}|Toks], Pos, eos);
		_  -> 
			P = ?IncPos(Parent),
			templ_scan:scan(Cs, [], [{r_delim,P}|Toks], P, State, Errors, ?LDelim)
	end;
sub_scan_delim(Cs, [Fun], Toks, Pos, State, Errors, _Delim) ->
												% not the delimiter
	Fun(Cs, [], Toks, Pos, State, Errors, ?RDelim);
sub_scan_delim(Cs, Stack, Toks, Pos, State, Errors, _Delim) ->
												% not the delimiter
    [Fun,StartPos|Buf] = reverse(Stack),
	Fun(Buf++Cs, [], Toks, ?ResetPos(Pos, StartPos), State, Errors, ?RDelim).


%% @doc the actual scan loop.
%%      stack is assumed to be []
%% 
main_scan([$\n|Cs], Stack, Toks, Pos, State, Errors, Delim) -> 
												% Newline - skip
    scan_rec(Cs, Stack, Toks, ?IncPos(Pos), State, Errors, Delim);
main_scan([C|Cs], Stack, Toks, Pos, State, Errors, Delim) 
  when C >= $\000, C =< $\s ->                  % Control chars - skip
    scan_rec(Cs, Stack, Toks, Pos, State, Errors, Delim);
main_scan([C|Cs], Stack, Toks, Pos, State, Errors, Delim) 
  when C >= $\200, C =< $\240 ->                % Control chars -skip
    scan_rec(Cs, Stack, Toks, Pos, State, Errors, Delim);

main_scan([C|Cs], _Stack, Toks, Pos, State, Errors, Delim) 
  when C >= $a, C =< $z ->                      % Names
    scan_name(Cs, [C], Toks, Pos, State, Errors, Delim);
main_scan([C|Cs], _Stack, Toks, Pos, State, Errors, Delim) 
  when C >= $A, C =< $Z ->                      % Names
    scan_name(Cs, [C], Toks, Pos, State, Errors, Delim);

main_scan([C|Cs], _Stack, Toks, Pos, State, Errors, Delim)
%    when C >= $0, C =< $9;
%         C =:= $- ->                            % Numbers 
    when C >= $0, C =< $9 ->                    % Numbers 
    scan_number(Cs, [C], Toks, Pos, State, Errors, Delim);

main_scan([$"|Cs], _Stack, Toks, Pos, State, Errors, Delim) ->      % String
    scan_string(Cs, [$",Pos], Toks, Pos, State, Errors, Delim);

main_scan([C1,$==C2|Cs], Stack, Toks, Pos, State, Errors, Delim) 
  when C1 =:= $=; C1 =:= $<; C1 =:= $>; C1 =:= $! -> % Logical operators
    scan_rec(Cs, Stack, [{list_to_atom([C1,C2]),Pos}|Toks], Pos, State, Errors, Delim);
main_scan([$&,$&|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_rec(Cs, Stack, [{'&&',Pos}|Toks], Pos, State, Errors, Delim);
main_scan([$|,$||Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_rec(Cs, Stack, [{'||',Pos}|Toks], Pos, State, Errors, Delim);

main_scan([$=|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
	scan_rec(Cs, Stack, [{'=',Pos}|Toks], Pos, State, Errors, Delim);

main_scan([C|Cs], Stack, Toks, Pos, State, Errors, Delim) 
  when C =:= $.; C =:= $[; C =:= $]; C =:= $(; C =:= $); C =:= $,;
       C =:= $|; C =:= $$; C =:= $@; C =:= $<; C =:= $>; C =:= $!; 
       C =:= $:; C =:= $+; C =:= $-; C =:= $*; C =:= $/ ->
    scan_rec(Cs, Stack, [{list_to_atom([C]),Pos}|Toks], Pos, State, Errors, Delim);

main_scan([C|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    more([C|Cs], Stack, Toks, Pos, State, Errors, Delim, fun scan/7);

main_scan([], Stack, Toks, Pos, State, Errors, Delim) ->
    more([], Stack, Toks, Pos, State, Errors, Delim, fun scan/7);
main_scan(Eof, _Stack, Toks, Pos, State, Errors, _Delim) ->
    done(Eof, Errors, Toks, Pos, State).


%% @doc scan for a name
%%      begin-with ::= [a-zA-Z], char ::= [a-zA-Z] | number | '_'
%%
scan_name([C|Cs], Stack, Toks, Pos, State, Errors, Delim) 
  when C >= $a, C =< $z; 
       C >= $A, C =< $Z; 
       C >= $0, C =< $9;
       C =:= $_ ->
	scan_name(Cs, [C|Stack], Toks, Pos, State, Errors, Delim);
scan_name([], Stack, Toks, Pos, State, Errors, Delim) ->
    more([], Stack, Toks, Pos, State, Errors, Delim, fun scan_name/7);
scan_name(Cs, Stack, Toks, Pos, State, Errors, Delim) ->
	Name = reverse(Stack),
	case catch list_to_atom(Name) of
		Atom when is_boolean(Atom) ->
			scan_rec(Cs, [], [{boolean,Pos,Atom}|Toks], Pos, State, Errors, Delim);
		Atom when is_atom(Atom) ->
			Token = case reserved_word(Atom) of
						true  -> {Atom,Pos};
						false -> {name,Pos,Name}
					end,
			scan_rec(Cs, [], [Token|Toks], Pos, State, Errors, Delim);
		_ ->
			scan_rec(Cs, [], Toks, Pos, State, [{{illegal,atom}}|Errors], Delim)
	end.

%% @doc scan for a string
%%
scan_string([$"|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    [StartPos,$"|S] = reverse(Stack),
    scan_rec(Cs, [], [{string,StartPos,S}|Toks], Pos, State, Errors, Delim);
scan_string([$\\|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    sub_scan_escape(Cs, Stack, Toks, Pos, State, Errors, Delim);
%scan_string([nl|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
%    scan_string(Cs, ["hoge"|Stack], Toks, ?IncPos(Pos), State, Errors, Delim);
scan_string([$\n|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_string(Cs, [$\n|Stack], Toks, ?IncPos(Pos), State, Errors, Delim);
scan_string([C|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_string(Cs, [C|Stack], Toks, Pos, State, Errors, Delim);
scan_string([], Stack, Toks, Pos, State, Errors, Delim) ->
    more([], Stack, Toks, Pos, State, Errors, Delim, fun scan_string/7);
scan_string(Eof, Stack, _Toks, Pos, State, Errors, _Delim) ->
    [StartPos,$"|S] = reverse(Stack),
    SS = string:substr(S, 1, 16),
    done(Eof, [{{string,$",SS},StartPos}|Errors], [], Pos, State).

%% @doc scan for a character escape sequence, in string. 
%%      a string is a syntactical sugar list (e.g "abc") 
%%
%% \X - escape sequences
sub_scan_escape([$"|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_string(Cs, [$"|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$\\|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_string(Cs, [$\\|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$/|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_string(Cs, [$/|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$b|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_string(Cs, [$\b|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$f|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_string(Cs, [$\f|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$n|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_string(Cs, [$\n|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$r|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_string(Cs, [$\r|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$t|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
    scan_string(Cs, [$\t|Stack], Toks, Pos, State, Errors, Delim);
%% \u<4> hex digits - escaped unicode chars
% TODO bit演算子を使う
sub_scan_escape([$u,H1,H2,H3,H4|Cs], Stack, Toks, Pos, State, Errors, Delim) 
  when H1 == $0, H2 == $0, H3 >= $0, H3 =< $7, ?IsHex(H4) ->
	H3v = read_hex(H3), H4v = read_hex(H4),
    Val = H3v*16 + H4v,
    scan_string(Cs, [Val|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$u,H1,H2,H3,H4|Cs], Stack, Toks, Pos, State, Errors, Delim) 
  when H1 == $0, H2 == $0, ?IsHexGT(H3, $8), ?IsHex(H4) ->
	H3v = read_hex(H3), H4v = read_hex(H4),
    V1 = 192 + (H3v div 4),
    V2 = 128 + (H3v rem 4)*16 + H4v,
    scan_string(Cs, [V2,V1|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$u,H1,H2,H3,H4|Cs], Stack, Toks, Pos, State, Errors, Delim) 
  when H1 == $0, H2 >= $1, H2 =< $7, ?IsHex(H3), ?IsHex(H4) ->
	H2v = read_hex(H2), H3v = read_hex(H3), H4v = read_hex(H4),
    V1 = 192 + H2v*4 + (H3v div 4),
    V2 = 128 + (H3v rem 4)*16 + H4v,
    scan_string(Cs, [V2,V1|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$u,H1,H2,H3,H4|Cs], Stack, Toks, Pos, State, Errors, Delim) 
  when H1 == $0, ?IsHexGT(H2, $8), ?IsHex(H3), ?IsHex(H4) ->
	H2v = read_hex(H2), H3v = read_hex(H3), H4v = read_hex(H4),
    V1 = 224,
    V2 = 128 + H2v*4 + (H3v div 4),
    V3 = 128 + (H3v rem 4)*16 + H4v,
    scan_string(Cs, [V3,V2,V1|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$u,H1,H2,H3,H4|Cs], Stack, Toks, Pos, State, Errors, Delim) 
  when ?IsHexGT(H1, $1), ?IsHex(H2), ?IsHex(H3), ?IsHex(H4) ->
	H1v = read_hex(H1), H2v = read_hex(H2), 
	H3v = read_hex(H3), H4v = read_hex(H4),
    V1 = 224 + H1v,
    V2 = 128 + H2v*4 + (H3v div 4),
    V3 = 128 + (H3v rem 4)*16 + H4v,
    scan_string(Cs, [V3,V2,V1|Stack], Toks, Pos, State, Errors, Delim);
sub_scan_escape([$u,H1,H2,H3]=Cs, Stack, Toks, Pos, State, Errors, Delim) 
  when ?IsHex(H1), ?IsHex(H2), ?IsHex(H3) ->
    more(Cs, Stack, Toks, Pos, State, Errors, Delim, fun sub_scan_escape/7);
sub_scan_escape([$u,H1,H2]=Cs, Stack, Toks, Pos, State, Errors, Delim) 
  when ?IsHex(H1), ?IsHex(H2) ->
    more(Cs, Stack, Toks, Pos, State, Errors, Delim, fun sub_scan_escape/7);
sub_scan_escape([$u,H1]=Cs, Stack, Toks, Pos, State, Errors, Delim) 
  when ?IsHex(H1) ->
    more(Cs, Stack, Toks, Pos, State, Errors, Delim, fun sub_scan_escape/7);
%%
sub_scan_escape([C|Cs], Stack, Toks, Pos, State, Errors, Delim) ->
	more([C|Cs], Stack, Toks, Pos, State, Errors, Delim, fun sub_scan_escape/7);
sub_scan_escape([], Stack, Toks, Pos, State, Errors, Delim) ->
    more([], Stack, Toks, Pos, State, Errors, Delim, fun sub_scan_escape/7);
sub_scan_escape(Eof, Stack, _Toks, Pos, State, Errors, _Delim) ->
    [StartPos,$"|S] = reverse(Stack),
    SS = string:substr(S, 1, 16),
    done(Eof, [{{string,$",SS},StartPos}|Errors], [], Pos, State).

read_hex(H) when H >= $0, H =< $9 -> 
	{V,_} = string:to_integer([H]), V;
read_hex(H) ->
	{_,[V],_} = io_lib:fread("~16u", [H]), V.

%% @doc scan numbers
%%
scan_number(Cs, [$0], Toks, Pos, State, Errors, Delim) ->
    scan_rec(Cs, [], [{integer,Pos,0}|Toks], Pos, State, Errors, Delim);
scan_number([$0|Cs], [$-], Toks, Pos, State, Errors, Delim) ->
    scan_rec(Cs, [], [{integer,Pos,-0}|Toks], Pos, State, Errors, Delim);
scan_number([C|Cs], Stack, Toks, Pos, State, Errors, Delim) when C >= $0, C =< $9 ->
    scan_number(Cs, [C|Stack], Toks, Pos, State, Errors, Delim);
scan_number([], Stack, Toks, Pos, State, Errors, Delim) ->
    more([], Stack, Toks, Pos, State, Errors, Delim, fun scan_number/7);
scan_number(Cs, Stack, Toks, Pos, State, Errors, Delim) ->
    case catch list_to_integer(reverse(Stack)) of
		N when is_integer(N) ->
			scan_rec(Cs, [], [{integer,Pos,N}|Toks], Pos, State, Errors, Delim);
		_ ->
			scan_rec(Cs, [], Toks, Pos, State, [{{illegal,integer},Pos}|Errors], Delim)
    end.


%% @spec reserved_word(atom()) -> bool()
%% @doc check if a word is reserved
%%
reserved_word('if') -> true;
reserved_word('ifdef') -> true;
reserved_word('else') -> true;
reserved_word('endif') -> true;
reserved_word('endifdef') -> true;
reserved_word('let') -> true;
reserved_word('is') -> true;
reserved_word('endlet') -> true;
reserved_word('foreach') -> true;
reserved_word('in') -> true;
reserved_word('endforeach') -> true;
reserved_word('end') -> true;
reserved_word('include') -> true;
reserved_word(_) -> false.
